﻿/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Threading;
using Winforms = System.Windows.Forms;
using Bio;
using Bio.IO.GenBank;
using Bio.Web.Blast;
using QUT.Bio.Util;

namespace DBLoader {
	/// <summary>
	/// Interaction logic for Window1.xaml
	/// </summary>
	public partial class MainWindow: Window, ILogWriter {
		#region Instance variables
		private DataLayer dataLayer;

		const string DB_XREF = "db_xref";
		static readonly string [] XREF_SPLIT = { ":" };

		private static FileInfo blast = new FileInfo( string.Format(
			"{0}\\blastall.exe",
			Configuration.Args["BlastBin"]
		) );

		private static FileInfo formatDB = new FileInfo( string.Format(
			"{0}\\formatdb.exe",
			Configuration.Args["BlastBin"]
		) );

		private static DirectoryInfo tempDir = new DirectoryInfo( Configuration.Args["TempDir"] );

		double eValue = 1.0e-15;
		double gapOpen = 11;
		double gapExtend = 1;

		#endregion

		public MainWindow () {
			InitializeComponent();

			dataLayer = new DataLayer( this );
		}

		#region Method: NewDatabase
		/// <summary>
		/// Event listener for "New database" menu item: creates a new SilverMap database.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>

		private void NewDatabase ( object sender, RoutedEventArgs e ) {
			Winforms.SaveFileDialog dialog = new Winforms.SaveFileDialog {
				Title = "Create SilverMap database",
				AddExtension = true,
				AutoUpgradeEnabled = true,
				DefaultExt = ".mdf",
				OverwritePrompt = true,
				SupportMultiDottedExtensions = true,
				Filter = "SQL Server database|*.mdf|All files|*.*"
			};

			if ( dialog.ShowDialog() == Winforms.DialogResult.OK ) {
				dataLayer.CreateDatabase( dialog.FileName );
			}
		}
		#endregion

		#region Method: Write
		/// <summary>
		/// Appends a message to the associated streamlike object, without appending a trailing EOL.
		/// </summary>
		/// <param name="format"></param>
		/// <param name="args"></param>

		public void Write ( string format, params object[] args ) {
			Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
				eventLog.Text += string.Format( format, args );
			} ) );
		}
		#endregion

		#region Method: WriteLine
		/// <summary>
		/// Appends an EOL marker to whatever streamlike object 
		/// </summary>

		public void WriteLine () {
			Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
				eventLog.Text += "\n";
			} ) );
		}
		#endregion

		#region Method: WriteLine( format, args, ... )
		/// <summary>
		/// Appends a message to the associated streamlike object and appends a trailing EOL marker.
		/// </summary>
		/// <param name="format"></param>
		/// <param name="args"></param>

		public void WriteLine ( string format, params object[] args ) {
			Write( format, args );
			WriteLine();
		}
		#endregion

		#region Method: ImportGenes( sender, eventArgs )
		/// <summary>
		/// Event listener for the "Import gene data" menu option: loads a summary 
		/// of genes from one or more genbank (text, .gbk) files into the Gene table.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>

		private void ImportGenes ( object sender, RoutedEventArgs e ) {
			if ( dataLayer.DatabaseName == String.Empty ) {
				Alert.Show( "Please connect to a SilverMap database before trying this!" );
				return;
			}

			Winforms.OpenFileDialog dialog = new System.Windows.Forms.OpenFileDialog {
				AutoUpgradeEnabled = true,
				CheckFileExists = true,
				DefaultExt = ".gbk",
				Filter = "Genbank text files|*.gbk|All files|*.*",
				Multiselect = true,
				SupportMultiDottedExtensions = true,
				Title = "Import gene data from Genbank file"
			};

			if ( dialog.ShowDialog() == Winforms.DialogResult.OK ) {
				statusBar.StartProgress();

				Thread backgroundWorker = new Thread( new ThreadStart( delegate() {
					try {
						foreach ( string fileName in dialog.FileNames ) {
							ImportGenes( fileName );
						}
					}
					finally {
						Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
							statusBar.StopProgress();
							statusBar.WriteLine( "Operation complete: {0} genomes processed.", dialog.FileNames.Length );
						} ) );
					}
				} ) );

				backgroundWorker.Start();
			}
		}
		#endregion

		#region Method: ImportGenes ( fileName )

		/// <summary>
		/// Loads a genbank document containing gene information and inserts it into the nominated silvermap database
		/// </summary>
		/// <param name="fileName">The name of a file to read; if the file name ends with '.gz' we use a GZipStream to decompress it on the fly.</param>

		private void ImportGenes ( string fileName ) {
			Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
				statusBar.WriteLine( "Importing genes from {0}", fileName );
			} ) );

			GenBankParser parser = new GenBankParser();

			TextReader reader = fileName.EndsWith( ".gz" ) 
				? new StreamReader( new GZipStream( new FileStream( fileName, FileMode.Open ), CompressionMode.Decompress ) )
				: new StreamReader( fileName );

			try {
				IList<ISequence> sequences = parser.Parse( reader );

				foreach ( ISequence sequence in sequences ) {
					ImportGenes( sequence );
				}
			}
			catch ( Exception e ) {
				WriteLine( "Error parsing genbank document '{0}':\n\t{1}", fileName, e.Message );
			}
		}
		#endregion

		#region Method: ImportGenes
		/// <summary>
		/// Imports the coding sequences contained in a sequence. 
		/// </summary>
		/// <param name="sequence"></param>

		private void ImportGenes ( ISequence sequence ) {
			GenBankMetadata metadata = sequence.Metadata["GenBank"] as GenBankMetadata;
			string accessionVersion = metadata.Version.CompoundAccession;
			string definition = metadata.Definition;

			GenomeDescriptor genome = new GenomeDescriptor {
				AccessionVersion = accessionVersion,
				Definition = definition
			};

			dataLayer.ImportOrganism( genome );

			int importedGeneCount = 0;

			foreach ( CodingSequence cds in metadata.Features.CodingSequences ) {
				Gene gene = new Gene {
					AccessionVersion = accessionVersion,
					LocusTag = cds.LocusTag.Count > 0 ? Unquote( cds.LocusTag[0] ) : "-",
					Symbol = Unquote( cds.GeneSymbol ),
					Note = Unquote( cds.Note.Join( "\n" ) ),
					Product = Unquote( cds.Product.Join( "\n" ) ),
					Translation = Unquote( cds.Translation ),
					ProteinId = cds.ProteinId.Count > 0 ? Unquote( cds.ProteinId[0] ) : "-"
				};

				if ( cds.Qualifiers.ContainsKey( DB_XREF ) ) {
					foreach ( string dbXref in cds.Qualifiers[DB_XREF] ) {
						string [] parts = dbXref.Replace( "\"", "" ).Split( XREF_SPLIT, StringSplitOptions.RemoveEmptyEntries );
						string db = parts[0];
						string xref = parts[1];

						switch ( db ) {
						case "COG":
							gene.Cog = xref;
							break;
						case "GeneID":
							gene.GeneId = xref;
							break;
						case "GI":
							gene.GenbankId = xref;
							break;
						default:
							break;
						}
					}
				}

				importedGeneCount += dataLayer.ImportGene( gene );
			}

			WriteLine( "{0} - {1}: {2} genes loaded", genome.AccessionVersion, genome.Definition, importedGeneCount );
		}
		#endregion

		#region Method: Unquote
		/// <summary>
		/// Strips a single leading and trailing double quote character from the supplied input string,
		/// returning the results.
		/// </summary>
		/// <param name="s">The supplied input string.</param>
		/// <returns>the input, with a single leading and trailing double quote removed.</returns>

		private static string Unquote ( string s ) {
			int startPos = 0, length = s.Length;

			if ( s.Length == 0 )
				return s;

			if ( s[0] == '"' ) {
				startPos++;
				length--;
			}

			if ( s[s.Length - 1] == '"' ) {
				length--;
			}

			return s.Substring( startPos, length );
		}
		#endregion

		#region Method: Blast
		/// <summary>
		/// Event listener for the "Blast" menu item: displays a genome chooser, then
		/// instigates an all-against all blast.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>

		private void Blast ( object sender, RoutedEventArgs e ) {
			if ( dataLayer.DatabaseName == string.Empty ) {
				Alert.Show( "Please connect to a SilverMap database before trying this." );
				return;
			}

			GenomeChooser genomeChooser = new GenomeChooser {
				EValue = eValue,
				GapOpen = gapOpen,
				GapExtend = gapExtend
			};
			genomeChooser.Init( dataLayer, this );

			if ( (bool) genomeChooser.ShowDialog() ) {
				string [] accessions = ( from x in genomeChooser.SelectedGenomes
										 select x.AccessionVersion ).ToArray();
				eValue = genomeChooser.EValue;
				gapOpen = genomeChooser.GapOpen;
				gapExtend = genomeChooser.GapExtend;

				statusBar.StartProgress();

				Thread backgroundWorker = new Thread( new ThreadStart( delegate() {
					try {
						BlastAllAgainstAll( accessions, eValue, gapOpen, gapExtend );
					}
					catch ( Exception ex ) {
						WriteLine( "Error doing BlastAllAgainstAll: {0}", ex.Message );
					}
					finally {
						Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
							statusBar.StopProgress();
							statusBar.WriteLine( "Operation complete." );
						} ) );
					}
				} ) );

				backgroundWorker.Start();
			}
		}

		#endregion

		#region Method: Exit
		/// <summary>
		/// Event listener for the Exit menu option: displays a "ok/Cancel" dialog, then exits if the user says "ok".
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>

		private void Exit ( object sender, RoutedEventArgs e ) {
			if ( MessageBox.Show( "Application will shut down." ) == MessageBoxResult.OK ) {
				Environment.Exit( 0 );
			}
		}

		#endregion

		#region Method: ConnectDatabase
		/// <summary>
		/// Event listener for the "Connect to database" menu item: 
		/// displays a list box that the user can use to choose an existing
		/// SilverMap database, and connects the data layer to the database selected by the user.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>

		private void ConnectDatabase ( object sender, RoutedEventArgs e ) {
			ConnectionChooser chooser = new ConnectionChooser();
			chooser.Init( dataLayer );

			if ( dataLayer.DatabaseName != string.Empty ) {
				chooser.DatabaseName = dataLayer.DatabaseName;
			}

			if ( (bool) chooser.ShowDialog() && chooser.DatabaseName != string.Empty ) {
				dataLayer.DatabaseName = chooser.DatabaseName;
				WriteLine( "Database set to {0}", chooser.DatabaseName );
			}
		}
		#endregion

		#region Method: Clear
		/// <summary>
		/// Event listener for the "Clear button": erases the contents of the error display.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>

		private void ClearEventLog ( object sender, RoutedEventArgs e ) {
			eventLog.Text = string.Empty;
		}
		#endregion

		#region Method: BlastAllAgainstAll
		/// <summary>
		/// Performs a multi-way BLAST between all genes in all organisms in the supplied 
		/// list of accession-version numbers.
		/// </summary>
		/// <param name="accessions">A list of genomes to process.</param>

		private void BlastAllAgainstAll ( string[] accessions, double eValue, double gapOpen, double gapExtend ) {
			Dictionary<string, FileInfo> fastaFiles = new Dictionary<string, FileInfo>();

			foreach ( string accession in accessions ) {
				SetUpBlastFiles( accession, fastaFiles );
			}

			foreach ( string queryAccession in accessions ) {
				foreach ( string subjectAccession in accessions ) {
					Blast( queryAccession, subjectAccession, fastaFiles, eValue, gapOpen, gapExtend );
				}
			}

			foreach ( string accession in accessions ) {
				CleanupBlastFiles( accession, fastaFiles );
			}
		}
		#endregion

		#region Method: SetUpBlastFiles
		/// <summary>
		/// Creates fasta query files and blast databases for a single genome, specified 
		/// by its accession-version identifier.
		/// </summary>
		/// <param name="accession">The accession-version number of a single genome.</param>
		/// <param name="fastaFiles">A lookup table in which the name of the created file will be saved for later use.</param>

		private void SetUpBlastFiles ( string accession, Dictionary<string, FileInfo> fastaFiles ) {

			FileInfo fastaFile = new FileInfo( string.Format( "{0}\\{1}.fasta", tempDir, accession ) );
			fastaFiles.Add( accession, fastaFile );

			StreamWriter fastaWriter = new StreamWriter( fastaFile.FullName );
			WriteLine( "Creating temp file '{0}'", fastaFile );

			#region Create fasta query file.
			try {
				dataLayer.GetTranslations( accession, delegate( decimal id, string translation ) {
					fastaWriter.WriteLine( ">{0}", id );
					fastaWriter.WriteLine( translation );
				} );
			}
			finally {
				try {
					fastaWriter.Close();
				}
				catch {
				}
			}

			#endregion

			ProcessWrapper formatDBProcess = null;
			WriteLine( "Running formatdb:" );

			#region Create blast database from fasta query file.
			try {
				formatDBProcess = new ProcessWrapper( formatDB, tempDir, null,
					"-i", fastaFile
				);

				formatDBProcess.Start();

				if ( !formatDBProcess.WaitForExit( 1 * 60 * 1000 ) ) {
					formatDBProcess.Kill();
				}

				WriteLine( "format db finished with exit code {0}", formatDBProcess.ExitCode );
				WriteLine( "\tstdout: {0}", formatDBProcess.Stdout );
				WriteLine( "\tstderr: {0}", formatDBProcess.Stderr );

				formatDBProcess.Close();
				formatDBProcess = null;
			}
			finally {
				if ( formatDBProcess != null ) {
					formatDBProcess.Kill();
					formatDBProcess = null;
				}
			}
			#endregion
		}
		#endregion

		#region Method: CleanupBlastFiles
		/// <summary>
		/// Removes fasta query file and blast database for a specified genome.
		/// </summary>
		/// <param name="accession">The accession-version identifier of the genome.</param>
		/// <param name="fastaFiles">A lookup table from which the base name of the fasta query and database files can be obtained.</param>

		private void CleanupBlastFiles ( string accession, Dictionary<string, FileInfo> fastaFiles ) {
			FileInfo [] files = {
				fastaFiles[accession],
				new FileInfo( fastaFiles[accession].FullName + ".pin" ),
				new FileInfo( fastaFiles[accession].FullName + ".phr" ),
				new FileInfo( fastaFiles[accession].FullName + ".psq" )
			};

			foreach ( FileInfo f in files ) {
				WriteLine( "Removing temp file: '{0}'", f );

				try {
					f.Delete();
				}
				catch ( Exception ex ) {
					WriteLine( "\tError removing '{0}': {1}", f, ex.Message );
				}
			}
		}
		#endregion

		#region Method: Blast
		/// <summary>
		/// Does one iteration of NCBI BLAST, testing all genes in the query genome against those in the
		/// subject genome.
		/// </summary>
		/// <param name="queryAccession"></param>
		/// <param name="subjectAccession"></param>
		/// <param name="fastaFile"></param>
		/// <param name="eValue"></param>
		/// <param name="gapExtend"></param>
		/// <param name="gapOpen"></param>

		private void Blast (
			string queryAccession,
			string subjectAccession,
			Dictionary<string, FileInfo> fastaFile,
			double eValue,
			double gapOpen,
			double gapExtend
		) {

			if ( dataLayer.GetHitCount( queryAccession, subjectAccession ) > 0 ) {
				WriteLine( "Skipping BLAST for {0} -> {1}: results already exist.", queryAccession, subjectAccession );
				return;
			}

			FileInfo outputXmlDoc = new FileInfo( string.Format( "{0}\\{1}.xml", tempDir, Guid.NewGuid() ) );
			ProcessWrapper blastProcess = null;

			try {
				blastProcess = new ProcessWrapper(
					blast, tempDir, null,
					"-p", "blastp",
					"-i", fastaFile[queryAccession].Name,
					"-d", fastaFile[subjectAccession].Name,
					"-o", outputXmlDoc.Name,
					"-m", 7,
					"-e", eValue,
					"-G", gapOpen,
					"-E", gapExtend
				);

				blastProcess.Start();

				if ( !blastProcess.WaitForExit( 10 * 60 * 1000 ) ) {
					blastProcess.Kill();
				}

				WriteLine( "blastall finished with exit code {0}", blastProcess.ExitCode );
				WriteLine( "\tstdout: {0}", blastProcess.Stdout );
				WriteLine( "\tstderr: {0}", blastProcess.Stderr );

				blastProcess.Close();
				blastProcess = null;

				ImportHits( outputXmlDoc );
			}
			finally {
				if ( blastProcess != null ) {
					blastProcess.Kill();
					blastProcess.Close();
				}
			}
		}
		#endregion

		private void ImportHits ( FileInfo outputXmlDoc ) {
            BlastXmlParser parser = new BlastXmlParser();
			IList<BlastResult> blastResults = parser.Parse( outputXmlDoc.FullName );

			foreach ( BlastResult blastResult in blastResults ) {
				foreach ( BlastSearchRecord blastSearchRecord in blastResult.Records ) {
					try {
						// formatdb puts the SilverMap id number in the def of the query and hit.
						decimal queryId = decimal.Parse( blastSearchRecord.IterationQueryDefinition );
						int queryLength = blastSearchRecord.IterationQueryLength;
						int rank = 0;

						foreach ( Bio.Web.Blast.Hit h in blastSearchRecord.Hits ) {
							decimal hitId = decimal.Parse( h.Def );

							if ( hitId == queryId ) {
								continue;
							}

							long hitLength = h.Length;

							foreach ( Hsp hsp in h.Hsps ) {
								rank++;

								Hit hit = new Hit {
									AlignmentLength = (int) hsp.AlignmentLength,
									BitScore = hsp.BitScore,
									EValue = hsp.EValue,
									HitFrame = (int) hsp.HitFrame,
									HitFrom = (int) hsp.HitStart,
									HitGaps = 0,
									HitId = hitId,
									HitLength = (int) hitLength,
									HitSequence = hsp.HitSequence,
									HitTo = (int) hsp.HitEnd,
									Identity = (int) hsp.IdentitiesCount,
									Midline = "Midline not available" /* hsp.Midline */,
									Positive = (int) hsp.PositivesCount,
									QueryFrame = (int) hsp.QueryFrame,
									QueryFrom = (int) hsp.QueryStart,
									QueryId = queryId,
									QueryLength = queryLength,
									QuerySequence = hsp.QuerySequence,
									QueryTo = (int) hsp.QueryEnd,
									Rank = rank,
									Score = (int) hsp.Score
								};

								dataLayer.ImportHit( hit );
							}
						}
					}
					catch ( Exception ex_ ) {
						WriteLine( "Error processing blast search record:" );
						WriteLine( ex_.ToString() );
					}
				}
			}
		}

		/// <summary>
		/// Event listener for the "Remove hits" menu item: Allows the user to select a list
		/// of genomes, then removes all hits with query or hit gene belonging to any of the nominated
		/// genomes.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Unblast ( object sender, RoutedEventArgs e ) {
			if ( dataLayer.DatabaseName == string.Empty ) {
				Alert.Show( "Please connect to a SilverMap database before trying this." );
				return;
			}
		
			int rowsRemoved = 0;
			
			BareGenomeChooser genomeChooser = new BareGenomeChooser();
			genomeChooser.Init( dataLayer, this );
			
			if ( (bool) genomeChooser.ShowDialog() ) {
				IEnumerable<string> accessions = from genomeDescriptor in genomeChooser.SelectedGenomes 
				select genomeDescriptor.AccessionVersion;
				
				foreach ( string accession in accessions ) {
					rowsRemoved += dataLayer.RemoveHits( accession );
				}
				
				WriteLine( "{0} entries removed", rowsRemoved );
			}
		}

		private void About ( object sender, RoutedEventArgs e ) {
			AboutBox1 aboutBox = new AboutBox1();
			aboutBox.ShowDialog();
		}

		private void Using ( object sender, RoutedEventArgs e ) {
			HelpWindow helpWindow = new HelpWindow();
			helpWindow.Show();
		}
	}
}
